/*
 * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/protocol/ReflectionSocketFactory.java,v 1.4 2004/12/21 23:15:21 olegk Exp $
 * $Revision: 480424 $
 * $Date: 2006-11-29 06:56:49 +0100 (Wed, 29 Nov 2006) $
 *
 * ====================================================================
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.commons.httpclient.protocol;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;

import org.apache.commons.httpclient.ConnectTimeoutException;

/**
 * This helper class uses refelction in order to execute Socket methods
 * available in Java 1.4 and above
 * 
 * @author Oleg Kalnichevski
 * 
 * @since 3.0
 */
public final class ReflectionSocketFactory {

	private static boolean REFLECTION_FAILED = false;

	private static Constructor INETSOCKETADDRESS_CONSTRUCTOR = null;
	private static Method SOCKETCONNECT_METHOD = null;
	private static Method SOCKETBIND_METHOD = null;
	private static Class SOCKETTIMEOUTEXCEPTION_CLASS = null;

	private ReflectionSocketFactory() {
		super();
	}

	/**
	 * This method attempts to execute Socket method available since Java 1.4
	 * using reflection. If the methods are not available or could not be
	 * executed <tt>null</tt> is returned
	 * 
	 * @param socketfactoryName
	 *            name of the socket factory class
	 * @param host
	 *            the host name/IP
	 * @param port
	 *            the port on the host
	 * @param localAddress
	 *            the local host name/IP to bind the socket to
	 * @param localPort
	 *            the port on the local machine
	 * @param timeout
	 *            the timeout value to be used in milliseconds. If the socket
	 *            cannot be completed within the given time limit, it will be
	 *            abandoned
	 * 
	 * @return a connected Socket
	 * 
	 * @throws IOException
	 *             if an I/O error occurs while creating the socket
	 * @throws UnknownHostException
	 *             if the IP address of the host cannot be determined
	 * @throws ConnectTimeoutException
	 *             if socket cannot be connected within the given time limit
	 * 
	 */
	public static Socket createSocket(final String socketfactoryName,
			final String host, final int port, final InetAddress localAddress,
			final int localPort, int timeout) throws IOException,
			UnknownHostException, ConnectTimeoutException {
		if (REFLECTION_FAILED) {
			// This is known to have failed before. Do not try it again
			return null;
		}
		// This code uses reflection to essentially do the following:
		//
		// SocketFactory socketFactory =
		// Class.forName(socketfactoryName).getDefault();
		// Socket socket = socketFactory.createSocket();
		// SocketAddress localaddr = new InetSocketAddress(localAddress,
		// localPort);
		// SocketAddress remoteaddr = new InetSocketAddress(host, port);
		// socket.bind(localaddr);
		// socket.connect(remoteaddr, timeout);
		// return socket;
		try {
			Class socketfactoryClass = Class.forName(socketfactoryName);
			Method method = socketfactoryClass.getMethod("getDefault",
					new Class[] {});
			Object socketfactory = method.invoke(null, new Object[] {});
			method = socketfactoryClass.getMethod("createSocket",
					new Class[] {});
			Socket socket = (Socket) method.invoke(socketfactory,
					new Object[] {});

			if (INETSOCKETADDRESS_CONSTRUCTOR == null) {
				Class addressClass = Class
						.forName("java.net.InetSocketAddress");
				INETSOCKETADDRESS_CONSTRUCTOR = addressClass
						.getConstructor(new Class[] { InetAddress.class,
								Integer.TYPE });
			}

			Object remoteaddr = INETSOCKETADDRESS_CONSTRUCTOR
					.newInstance(new Object[] { InetAddress.getByName(host),
							new Integer(port) });

			Object localaddr = INETSOCKETADDRESS_CONSTRUCTOR
					.newInstance(new Object[] { localAddress,
							new Integer(localPort) });

			if (SOCKETCONNECT_METHOD == null) {
				SOCKETCONNECT_METHOD = Socket.class.getMethod("connect",
						new Class[] { Class.forName("java.net.SocketAddress"),
								Integer.TYPE });
			}

			if (SOCKETBIND_METHOD == null) {
				SOCKETBIND_METHOD = Socket.class
						.getMethod("bind", new Class[] { Class
								.forName("java.net.SocketAddress") });
			}
			SOCKETBIND_METHOD.invoke(socket, new Object[] { localaddr });
			SOCKETCONNECT_METHOD.invoke(socket, new Object[] { remoteaddr,
					new Integer(timeout) });
			return socket;
		} catch (InvocationTargetException e) {
			Throwable cause = e.getTargetException();
			if (SOCKETTIMEOUTEXCEPTION_CLASS == null) {
				try {
					SOCKETTIMEOUTEXCEPTION_CLASS = Class
							.forName("java.net.SocketTimeoutException");
				} catch (ClassNotFoundException ex) {
					// At this point this should never happen. Really.
					REFLECTION_FAILED = true;
					return null;
				}
			}
			if (SOCKETTIMEOUTEXCEPTION_CLASS.isInstance(cause)) {
				throw new ConnectTimeoutException(
						"The host did not accept the connection within timeout of "
								+ timeout + " ms", cause);
			}
			if (cause instanceof IOException) {
				throw (IOException) cause;
			}
			return null;
		} catch (Exception e) {
			REFLECTION_FAILED = true;
			return null;
		}
	}
}
